﻿using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using theeg.GraphWpf.Arrangement;
using theeg.GraphWpf.Models;

namespace theeg.GraphWpf.Views
{
    public class TransitionToGeometryConverter : IValueConverter
    {
        private double _cost;
        private double _sint;

        public TransitionToGeometryConverter()
        {
            CapitalWidth = 5;
            CapitalHeight = 10;
        }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Transition transition = value as Transition;
            if (transition == null)
            {
                return null;
            }

            var sourceNode = transition.Source;
            var targetNode = transition.Target;

            DetermineControlPoints(transition);
            Point startPoint = transition.RoutingPoints[0];
            Point endPoint = transition.RoutingPoints[transition.RoutingPoints.Count - 1];

            GeometryGroup group = new GeometryGroup();

            //draw line
            PathGeometry path = new PathGeometry();
            PathFigure figure = new PathFigure();
            path.Figures.Add(figure);

            // special handling for self references
            figure.IsFilled = false;
            if (sourceNode == targetNode)
            {
                startPoint = CalculateSelfReference(startPoint, figure);
            }
            else
            {
                if (transition.TransitionStyle == TransitionStyle.Bezier)
                {
                    CalculateBezierSpline(transition, startPoint, figure);
                }
                else
                {
                    CalculatePolyLine(transition, ref startPoint, ref endPoint, figure);
                }
            }

            group.Children.Add(path);

            CalculateLineCapitals(transition, sourceNode, targetNode, ref startPoint, ref endPoint, group);
            return group;
        }

        private static void CalculatePolyLine(Transition transition, ref Point startPoint, ref Point endPoint, PathFigure figure)
        {
            figure.StartPoint = startPoint;
            PointCollection linePts = new PointCollection();
            for (int i = 1; i < transition.RoutingPoints.Count - 1; i++)
            {
                linePts.Add(transition.RoutingPoints[i]);
            }
            figure.Segments.Add(new PolyLineSegment
            {
                Points = linePts
            });
            figure.Segments.Add(new LineSegment
            {
                Point = endPoint
            });
        }

        private void CalculateLineCapitals(Transition transition, INode sourceNode, INode targetNode, ref Point startPoint, ref Point endPoint, GeometryGroup group)
        {
            //draw source arrow
            if (transition.SourceCapital != TransitionCapital.Plain && sourceNode != targetNode)
            {
                group.Children.Add(CalculateHeadAndTail(transition.SourceCapital, startPoint,
                    transition.RoutingPoints.Count <= 2 ? endPoint : transition.RoutingPoints[1]));
                if ((int)transition.SourceCapital >= 8)
                {
                    group.Children.Add(CalculateExtraHeader(startPoint, transition.SourceCapital));
                }
            }

            //draw target arrow
            if (transition.TargetCapital != TransitionCapital.Plain)
            {
                if (sourceNode == targetNode)
                {
                    Geometry head = CalculateHeadAndTail(transition.TargetCapital, startPoint,
                        transition.RoutingPoints.Count <= 2 ? startPoint : transition.RoutingPoints[transition.RoutingPoints.Count - 2]);
                    if (head != null)
                    {
                        group.Children.Add(head);
                        if ((int)transition.TargetCapital >= 8)
                        {
                            group.Children.Add(CalculateExtraHeader(startPoint, transition.TargetCapital));
                        }
                    }
                }
                else
                {
                    Geometry head = CalculateHeadAndTail(transition.TargetCapital, endPoint,
                        transition.RoutingPoints.Count <= 2 ? startPoint : transition.RoutingPoints[transition.RoutingPoints.Count - 2]);
                    if (head != null)
                    {
                        group.Children.Add(head);
                        if ((int)transition.TargetCapital >= 8)
                        {
                            group.Children.Add(CalculateExtraHeader(endPoint, transition.TargetCapital));
                        }
                    }
                }
            }
        }

        private static void DetermineControlPoints(Transition transition)
        {
            RoutingMediator.CalculateRouting(transition);
        }

        private static Point CalculateSelfReference(Point startPoint, PathFigure figure)
        {
            Point refPt = new Point(startPoint.X, startPoint.Y - 12.5);
            figure.StartPoint = new Point(refPt.X, refPt.Y + 12.5);
            figure.Segments.Add(new BezierSegment
            {
                Point1 = new Point(refPt.X, refPt.Y + 5.66547625),
                Point2 = new Point(refPt.X + 5.66547625, refPt.Y),
                Point3 = new Point(refPt.X + 12.5, refPt.Y)
            });
            figure.Segments.Add(new BezierSegment
            {
                Point1 = new Point(refPt.X + 19.33452425, refPt.Y),
                Point2 = new Point(refPt.X + 25.0, refPt.Y + 5.66547625),
                Point3 = new Point(refPt.X + 25.0, refPt.Y + 12.5)
            });
            figure.Segments.Add(new BezierSegment
            {
                Point1 = new Point(refPt.X + 25.0, refPt.Y + 19.33452425),
                Point2 = new Point(refPt.X + 19.33452425, refPt.Y + 25.0),
                Point3 = new Point(startPoint.X + 12.5, refPt.Y + 25.0)
            });
            figure.Segments.Add(new BezierSegment
            {
                Point1 = new Point(refPt.X + 5.66547625, refPt.Y + 25.0),
                Point2 = new Point(refPt.X, refPt.Y + 19.33452425),
                Point3 = new Point(refPt.X, refPt.Y + 12.5)
            });
            return startPoint;
        }

        private static void CalculateBezierSpline(Transition transition, Point startPoint, PathFigure figure)
        {
            Point[] firstPoints;
            Point[] secondPoints;
            var points1 = transition.RoutingPoints.ToArray();
            var points = new Point[points1.Length * 2];
            {
                var point2 = points1[0];
                var point3 = points1[1];
                points[0] = points1[0];
                points[1] = new Point(point2.X + (point3.X - point2.X) * 0.25, point2.Y + (point3.Y - point2.Y) * 0.25);
            }
            {
                var point1 = points1[points1.Length - 2];
                var point2 = points1[points1.Length - 1];
                points[points.Length - 2] = new Point(point1.X + (point2.X - point1.X) * 0.75, point1.Y + (point2.Y - point1.Y) * 0.75);
                points[points.Length - 1] = points1[points1.Length - 1];
            }
            for (int i = 1; i < points1.Length - 1; i++)
            {
                var point1 = points1[i - 1];
                var point2 = points1[i];
                var point3 = points1[i + 1];
                points[2 * i] = new Point(point1.X + (point2.X - point1.X) * 0.75, point1.Y + (point2.Y - point1.Y) * 0.75);
                points[2 * i + 1] = new Point(point2.X + (point3.X - point2.X) * 0.25, point2.Y + (point3.Y - point2.Y) * 0.25);
            }
            GetCurveControlPoints(points, out firstPoints, out secondPoints);
            PathSegmentCollection lines = new PathSegmentCollection();
            for (int i = 0; i < firstPoints.Length; ++i)
            {
                lines.Add(new BezierSegment(firstPoints[i], secondPoints[i], points[i + 1], true));
            }
            figure.StartPoint = startPoint;
            figure.Segments = lines;
            figure.IsClosed = false;
        }

        private Geometry CalculateHeadAndTail(TransitionCapital ts, Point start, Point end)
        {
            double theta = Math.Atan2(end.Y - start.Y, end.X - start.X);
            _sint = Math.Sin(theta);
            _cost = Math.Cos(theta);

            PathFigure figure = null;
            switch ((int)ts & 7)
            {
                case 0: //plain line
                    break;
                case 1: //arrow
                    figure = CreateCapitalArrow(start);
                    break;
                case 2: //trident
                    figure = CreateCapitalTrident(start);
                    break;
                case 4: //rectangle
                    figure = CreateCapitalRectangle(start);
                    break;
            }
            PathGeometry path = new PathGeometry();
            path.FillRule = FillRule.Nonzero;
            path.Figures.Add(figure);

            return path;
        }

        //Draw extra part of the arrow.
        private Geometry CalculateExtraHeader(Point start, TransitionCapital style)
        {
            // Parameter 0.5 or 1.5 is used to setup the distance that the HeaderExtra should be drawn.
            if (((int)style & 8) == 8)
            {
                //draw cross
                Point l = new Point(
                    start.X + (CapitalHeight * 1.5 * _cost - CapitalWidth * _sint),
                    start.Y + (CapitalHeight * 1.5 * _sint + CapitalWidth * _cost));

                Point r = new Point(
                    start.X + (CapitalHeight * 1.5 * _cost + CapitalWidth * _sint),
                    start.Y - (CapitalWidth * _cost - CapitalHeight * 1.5 * _sint));

                LineGeometry geometry = new LineGeometry
                {
                    StartPoint = l,
                    EndPoint = r
                };

                return geometry;
            }
            else if (((int)style & 16) == 16)
            {
                //draw circle
                Point center = new Point(
                    start.X + CapitalHeight * 1.5 * _cost,
                    start.Y + CapitalHeight * 1.5 * _sint);
                EllipseGeometry geometry = new EllipseGeometry
                {
                    Center = center,
                    RadiusX = CapitalWidth * 0.5,
                    RadiusY = CapitalWidth * 0.5
                };
                return geometry;
            }

            return null;
        }

        //draw rectangle
        private PathFigure CreateCapitalRectangle(Point start)
        {
            //parameter 6 is changed to -6 for offset
            Point p1 = new Point(
                start.X - 3 * _sint,
                start.Y + 3 * _cost);

            Point p2 = new Point(
                start.X + 3 * _sint,
                start.Y - 3 * _cost);

            Point p3 = new Point(
                start.X + (6 * _cost - 3 * _sint),
                start.Y + (6 * _sint + 3 * _cost));

            Point p4 = new Point(
                start.X + (6 * _cost + 3 * _sint),
                start.Y - (3 * _cost - 6 * _sint));

            PathFigure figure = new PathFigure();
            figure.StartPoint = p1;
            figure.IsClosed = true;
            figure.IsFilled = false;
            figure.Segments.Add(new LineSegment { Point = p2 });
            figure.Segments.Add(new LineSegment { Point = p4 });
            figure.Segments.Add(new LineSegment { Point = p3 });
            return figure;
        }

        //draw trident
        private PathFigure CreateCapitalTrident(Point start)
        {
            Point l = new Point(
                start.X - CapitalWidth * _sint,
                start.Y + CapitalWidth * _cost);

            Point r = new Point(
                start.X + CapitalWidth * _sint,
                start.Y - CapitalWidth * _cost);

            Point t = new Point(
                start.X + CapitalHeight * _cost,
                start.Y + CapitalHeight * _sint);

            PathFigure figure = new PathFigure();
            figure.StartPoint = start;
            figure.IsClosed = true;
            figure.IsFilled = false;
            figure.Segments.Add(new LineSegment { Point = l });
            figure.Segments.Add(new LineSegment { Point = t });
            figure.Segments.Add(new LineSegment { Point = r });
            return figure;
        }

        //draw arrow
        private PathFigure CreateCapitalArrow(Point start)
        {
            Point l = new Point(
                start.X + (CapitalHeight * _cost - CapitalWidth * _sint),
                start.Y + (CapitalHeight * _sint + CapitalWidth * _cost));

            Point r = new Point(
                start.X + (CapitalHeight * _cost + CapitalWidth * _sint),
                start.Y - (CapitalWidth * _cost - CapitalHeight * _sint));

            PathFigure figure = new PathFigure();
            figure.StartPoint = start;
            figure.IsClosed = true;
            figure.IsFilled = true;
            figure.Segments.Add(new LineSegment { Point = l });
            figure.Segments.Add(new LineSegment { Point = r });
            return figure;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        public double CapitalHeight { get; set; }

        public double CapitalWidth { get; set; }

        /// <summary>
        /// Get open-ended Bezier Spline Control Points.
        /// </summary>
        /// <param name="knots">Input Knot Bezier spline points.</param>
        /// <param name="firstControlPoints">Output First Control points
        /// array of knots.Length - 1 length.</param>
        /// <param name="secondControlPoints">Output Second Control points
        /// array of knots.Length - 1 length.</param>
        /// <exception cref="ArgumentNullException"><paramref name="knots"/>
        /// parameter must be not null.</exception>
        /// <exception cref="ArgumentException"><paramref name="knots"/>
        /// array must contain at least two points.</exception>
        public static void GetCurveControlPoints(Point[] knots,
            out Point[] firstControlPoints, out Point[] secondControlPoints)
        {
            if (knots == null || knots.Length < 2)
            {
                throw new ArgumentNullException("knots");
            }
            int n = knots.Length - 1;

            if (n == 1)
            {
                // Special case: Bezier curve should be a straight line.
                firstControlPoints = new Point[n];

                // 3P1 = 2P0 + P3
                firstControlPoints[0].X = (2 * knots[0].X + knots[1].X) / 3;
                firstControlPoints[0].Y = (2 * knots[0].Y + knots[1].Y) / 3;
                secondControlPoints = new Point[1];

                // P2 = 2P1 – P0
                secondControlPoints[0].X = 2 * firstControlPoints[0].X - knots[0].X;
                secondControlPoints[0].Y = 2 * firstControlPoints[0].Y - knots[0].Y;
                return;
            }

            // Calculate first Bezier control points
            // Right hand side vector
            double[] rhs = new double[n];

            // Set right hand side X values
            for (int i = 1; i < n - 1; ++i)
            {
                rhs[i] = 4 * knots[i].X + 2 * knots[i + 1].X;
            }
            rhs[0] = knots[0].X + 2 * knots[1].X;
            rhs[n - 1] = (8 * knots[n - 1].X + knots[n].X) / 2.0;

            double[] x = GetFirstControlPoints(rhs);

            // Set right hand side Y values
            for (int i = 1; i < n - 1; ++i)
            {
                rhs[i] = 4 * knots[i].Y + 2 * knots[i + 1].Y;
            }
            rhs[0] = knots[0].Y + 2 * knots[1].Y;
            rhs[n - 1] = (8 * knots[n - 1].Y + knots[n].Y) / 2.0;

            double[] y = GetFirstControlPoints(rhs);

            // Fill output arrays.
            firstControlPoints = new Point[n];
            secondControlPoints = new Point[n];
            for (int i = 0; i < n; ++i)
            {
                firstControlPoints[i] = new Point(x[i], y[i]);
                if (i < n - 1)
                {
                    secondControlPoints[i] = new Point(2 * knots[i + 1].X - x[i + 1],
                        2 * knots[i + 1].Y - y[i + 1]);
                }
                else
                {
                    secondControlPoints[i] = new Point((knots[n].X + x[n - 1]) / 2,
                        (knots[n].Y + y[n - 1]) / 2);
                }
            }
        }

        /// <summary>
        /// Solves a tridiagonal system for one of coordinates (x or y)
        /// of first Bezier control points.
        /// </summary>
        /// <param name="rhs">Right hand side vector.</param>
        /// <returns>Solution vector.</returns>
        private static double[] GetFirstControlPoints(double[] rhs)
        {
            int n = rhs.Length;
            double[] x = new double[n];
            double[] tmp = new double[n];

            double b = 2.0;
            x[0] = rhs[0] / b;

            // Decomposition and forward substitution.
            for (int i = 1; i < n; i++)
            {
                tmp[i] = 1 / b;
                b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
                x[i] = (rhs[i] - x[i - 1]) / b;
            }
            // Backsubstitution.
            for (int i = 1; i < n; i++)
            {
                x[n - i - 1] -= tmp[n - i] * x[n - i];
            }

            return x;
        }
    }
}
